// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:analysis_server/lsp_protocol/protocol.dart' show jsonRpcVersion;
import 'package:analysis_server/src/lsp/lsp_packet_transformer.dart';
import 'package:analysis_server/src/server/driver.dart';
import 'package:analysis_server/src/session_logger/log_entry.dart';

/// The driver used to communicate with the analysis server.
class ServerDriver {
  /// The protocol being used by the server.
  final ServerProtocol _protocol;

  /// A list of arguments from the command-line used to start the server.
  final List<String> arguments;

  /// The sink used to send messages from the IDE to the server's stdin, or
  /// `null` if the server has not been started using [start].
  IOSink? _stdinSink;

  /// The socket used to send messages from DTD to the server, or `null` if the
  /// server has not been connected to DTD using [connectToDtd].
  WebSocket? _dtdSocket;

  /// Stream controller for analysis server output messages.
  final StreamController<Message> _serverMessagesController =
      StreamController();

  /// Creates a new driver that can be used to communicate with a server.
  ///
  /// When the server is [start]ed, it will use the given [protocol].
  ///
  /// The server is run in a separate process.
  // TODO(brianwilkerson): Add a flag controlling whether the server is in the
  //  same process as the driver or in a separate process.
  factory ServerDriver({required List<String> arguments}) {
    var parsedArgs = Driver.createArgParser().parse(arguments);

    var protocolOption = parsedArgs.option(Driver.serverProtocolOption);
    var protocol = switch (protocolOption) {
      Driver.protocolAnalyzer => ServerProtocol.legacy,
      Driver.protocolLsp => ServerProtocol.lsp,
      null => throw StateError('No protocol specified'),
      _ => throw StateError('Unrecognized protocol $protocolOption'),
    };

    var useFineDependencies = parsedArgs.wasParsed(
      Driver.withFineDependenciesOption,
    );

    return ServerDriver._(
      arguments: [
        '--${Driver.serverProtocolOption}=${protocol.flagValue}',
        if (useFineDependencies) '--${Driver.withFineDependenciesOption}',
      ],
      protocol: protocol,
    );
  }

  /// Creates a new driver that can be used to communicate with a server.
  ///
  /// When the server is [start]ed, it will use the given [protocol].
  ///
  /// The server is run in a separate process.
  ServerDriver._({required this.arguments, required ServerProtocol protocol})
    : _protocol = protocol;

  /// The messages read from the analysis server's stdout.
  Stream<Message> get serverMessages => _serverMessagesController.stream;

  /// Returns the path to the `dart` executable.
  String get _dartExecutable {
    return Platform.resolvedExecutable;
  }

  /// Create a websocket through which DTD messages can be sent to the server
  /// and send a request to the server asking it to connect to the socket.
  // ignore: unnecessary_async
  Future<void> connectToDtd() async {
    // TODO(brianwilkerson): Implement this.
    throw UnimplementedError();
    // var socketUri = 'ws://';
    // _dtdSocket = await WebSocket.connect(socketUri);
  }

  /// Send an exit request to the server, then close the communication channels
  /// used to communicate with the server.
  void exit() {
    if (_protocol == ServerProtocol.lsp) {
      sendMessageFromIde(
        Message({
          'id': 0,
          'method': 'exit',
          'jsonrpc': jsonRpcVersion,
          'clientRequestTime': DateTime.now().millisecondsSinceEpoch,
        }),
      );
    }
    _stdinSink?.close();
    _stdinSink = null;
    _dtdSocket?.close();
    _dtdSocket = null;
    _serverMessagesController.close();
  }

  void sendMessageFromDTD(Message message) {
    if (_dtdSocket case var socket?) {
      socket.add(json.encode(message));
    } else {
      throw StateError(
        "The method 'connectToDtd' must be invoked before "
        'messages can be sent from DTD.',
      );
    }
  }

  void sendMessageFromFileWatcher(Message message) {
    // TODO(brianwilkerson): Implementing this will require some additional
    //  support in the server. The most likely approach is to add support for a
    //  new protocol that will fake receiving a watch event.
  }

  /// Send the given [message] to the server using the communication channel
  /// used by the IDE.
  void sendMessageFromIde(Message message) {
    if (_stdinSink case IOSink writeSink) {
      var jsonEncodedBody = jsonEncode(message);
      if (_protocol == ServerProtocol.lsp) {
        var utf8EncodedBody = utf8.encode(jsonEncodedBody);
        var header =
            'Content-Length: ${utf8EncodedBody.length}\r\n'
            'Content-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n';
        var asciiEncodedHeader = ascii.encode(header);

        // Header is always ascii, body is always utf8!
        writeSink.add(asciiEncodedHeader);
        writeSink.add(utf8EncodedBody);
      } else {
        writeSink.write(jsonEncodedBody);
      }
    } else {
      throw StateError(
        "The method 'start' must be invoked before "
        'messages can be sent from the IDE.',
      );
    }
  }

  void sendMessageFromPluginIsolate(Message message) {
    // TODO(brianwilkerson): Implementing this will require some additional
    //  support in the server. Two possibilities to consider:
    //  1. Add a protocol to fake receiving a messsage from the plugin isolate.
    //  2. Add a protocol to connect to a web socket as if it were a connection
    //     to a plugin isolate.
    //  The first is likely to be the easier path forward.
  }

  /// Send a shutdown request to the server.
  void shutdown() {
    if (_protocol == ServerProtocol.legacy) {
      sendMessageFromIde(
        Message({
          'id': 0,
          'method': 'server.shutdown',
          'clientRequestTime': DateTime.now().millisecondsSinceEpoch,
        }),
      );
    } else if (_protocol == ServerProtocol.lsp) {
      sendMessageFromIde(
        Message({
          'id': 0,
          'method': 'shutdown',
          'jsonrpc': jsonRpcVersion,
          'clientRequestTime': DateTime.now().millisecondsSinceEpoch,
        }),
      );
    }
  }

  /// Create and start the server.
  Future<void> start() async {
    if (_stdinSink != null) {
      throw StateError(
        'Analysis server already started, only one active instance is allowed '
        'at a time.',
      );
    }
    var process = await Process.start(_dartExecutable, [
      'language-server',
      '--suppress-analytics',
      ...arguments,
    ]);
    _stdinSink = process.stdin;
    if (_protocol == ServerProtocol.lsp) {
      process.stdout
          .transform(LspPacketTransformer())
          .listen(_receiveMessageFromServer);
    } else {
      process.stdout
          .transform(const Utf8Decoder())
          .transform(const LineSplitter())
          .listen(_receiveMessageFromServer);
    }
  }

  void _receiveMessageFromServer(String message) {
    if (_serverMessagesController.isClosed) {
      stderr.writeln('Got analysis server message after shutdown:\n$message');
    } else {
      _serverMessagesController.add(jsonDecode(message) as Message);
    }
  }
}

/// An indication of the protocol to be used when communicating with the server.
enum ServerProtocol {
  legacy('analyzer'),
  lsp('lsp');

  final String flagValue;
  const ServerProtocol(this.flagValue);
}
